package tw.com.hitevision.whiteboard.android.widgets.MeasurementTools;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.text.TextPaint;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import tw.com.hitevision.whiteboard.android.R;
import tw.com.hitevision.whiteboard.android.options.OptionsRecyclerViewAdapter;
import tw.com.hitevision.whiteboard.android.state.WhiteboardState;
import tw.com.hitevision.whiteboard.android.util.TextLayoutUtil;
import tw.com.hitevision.whiteboard.android.widgets.OptionsRulerMenuAdapter;

import static tw.com.hitevision.whiteboard.android.widgets.MeasurementTools.MeasurementValues.ROTATE_SKIP;
import static tw.com.hitevision.whiteboard.android.widgets.MeasurementTools.MeasurementValues.ROTATE_UNIT;
import static tw.com.hitevision.whiteboard.android.widgets.OptionsRulerMenuAdapter.RULER;
import static tw.com.hitevision.whiteboard.android.widgets.OptionsRulerMenuAdapter.TRIANGLE_RULER_30;

public class Ruler extends BasicRuler {
    private static final float INCH_TO_CM = 2.54f;

    /*updated by zf*/
    private OptionsRecyclerViewAdapter.OptionsRecyclerViewItemListener optionsListener;
    private float rulerMinLength;
    private float rulerMaxLength;
    private float rulerDefaultWidth;
    private float rulerDefaultHeight;
    private float frameStrokeWidth;
    private Paint rectPaint;
    private Paint rulerBackgroundPaint;
    private Paint measurePaint;
    private Paint detectLinePaint;
    private PointF startPointF;
    private PointF startP;
    private PointF[] rectPoints;
    private Path rulerRectPath;
    private boolean reset = true;
    private float detectLineStrokeWidth;
    private Region detectLineRegionTop;
    private Region detectLineRegionBottom;
    private Region rulerRegion;
    private boolean update = true;
    private float cmLong;
    private float cmMiddle;
    private float cmShort;
    private float cmMeasurement;
    private float rulerStartPadding;
    private float inchMeasurement;
    private float textRectSize;
    private TextPaint textPaint;
    private Drawable closeBtn;
    private Drawable extendBtn;
    private Drawable extendShortExtendBtn;
    private Drawable extendShortenBtn;
    private Drawable extendExtendBtn;
    private Drawable rotateBtn;
    private float btnTopMargin;
    private float btnStartMargin;
    private float btnMargin;
    private float btnSize;
    private PointF closeP = new PointF();
    private PointF extendP = new PointF();
    private PointF rotateP = new PointF();
    private float canvasDegree = 0f;
    private float totalDegree = 0f;
    private float measurementRectWidth;
    private float measurementRectHeight;
    private float measurementRectRadius;
    private Paint measurementRectPaint;
    private TextPaint measurementRectTextPaint;

    private float dragX = 0;
    private float dragY = 0;
    private float dragX2 = 0;
    private float dragY2 = 0;
    private MeasurementAction rulerAction = MeasurementAction.MOVE;

    private RulerLinePainter rulerLinePainter;
    private MeasurementToolListener rulerListener;
    private float bottomBarHeight;
    private float translateX = 0f;
    private float translateY = 0f;
    private String measurementText = "";
    private PointF motionCenterP = new PointF();

    public Ruler(Context context, MeasurementToolListener listener, OptionsRecyclerViewAdapter.OptionsRecyclerViewItemListener optionsListener) {
        super(context);
        name = RULER;
        this.rulerListener = listener;
        this.optionsListener = optionsListener;

        // init frame stroke
        @ColorInt int frameStrokeColor = context.getResources().getColor(R.color.ruler_frame_stroke_color);
        frameStrokeWidth = context.getResources().getDimensionPixelOffset(R.dimen.ruler_frame_stroke_width);
        rectPaint = initStrokePaint();
        rectPaint.setColor(frameStrokeColor);
        rectPaint.setStrokeWidth(frameStrokeWidth);

        rulerBackgroundPaint = initStrokePaint();
        rulerBackgroundPaint.setColor(getResources().getColor(R.color.ruler_background_color));
        rulerBackgroundPaint.setStyle(Paint.Style.FILL_AND_STROKE);

        measurePaint = initStrokePaint();
        measurePaint.setColor(frameStrokeColor);
        measurePaint.setStrokeWidth(frameStrokeWidth);

        measurementRectWidth = getResources().getDimensionPixelOffset(R.dimen.ruler_measurement_rect_width);
        measurementRectHeight = getResources().getDimensionPixelOffset(R.dimen.ruler_measurement_rect_height);
        measurementRectRadius = getResources().getDimensionPixelOffset(R.dimen.ruler_measurement_rect_radius);
        measurementRectPaint = initStrokePaint();
        measurementRectPaint.setStyle(Paint.Style.FILL);
        measurementRectPaint.setColor(getResources().getColor(R.color.ruler_measurement_background_color));

        measurementRectTextPaint = new TextPaint();
        measurementRectTextPaint.setAntiAlias(true);
        measurementRectTextPaint.setColor(getResources().getColor(R.color.hitevision_blue));
        measurementRectTextPaint.setTextSize(getResources().getDimensionPixelOffset(R.dimen.ruler_text_font_size));

        // init detect line
        @ColorInt int detectColor = context.getResources().getColor(R.color.ruler_detect_stroke_color);
        detectLineStrokeWidth = context.getResources().getDimensionPixelOffset(R.dimen.ruler_detect_range);
        detectLinePaint = initStrokePaint();
        detectLinePaint.setColor(detectColor);
        detectLinePaint.setStrokeWidth(detectLineStrokeWidth);

        textRectSize = context.getResources().getDimensionPixelSize(R.dimen.ruler_text_rect_size);
        textPaint = new TextPaint();
        textPaint.setColor(context.getResources().getColor(R.color.black));
        textPaint.setTextSize(context.getResources().getDimensionPixelSize(R.dimen.ruler_text_font_size));
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setAntiAlias(true);

        rectPoints = new PointF[4];
        for (int i = 0; i < rectPoints.length; i++) {
            rectPoints[i] = new PointF();
        }

        rulerDefaultWidth = context.getResources().getDimensionPixelOffset(R.dimen.ruler_default_width);
        rulerDefaultHeight = context.getResources().getDimensionPixelOffset(R.dimen.ruler_default_height);
        rulerMinLength = getResources().getDimensionPixelOffset(R.dimen.ruler_min_length);
        rulerMaxLength = getResources().getDimensionPixelOffset(R.dimen.ruler_max_length);

        cmLong = context.getResources().getDimensionPixelOffset(R.dimen.ruler_unit_long);
        cmMiddle = context.getResources().getDimensionPixelOffset(R.dimen.ruler_unit_middle);
        cmShort = context.getResources().getDimensionPixelOffset(R.dimen.ruler_unit_short);
        rulerStartPadding = context.getResources().getDimensionPixelOffset(R.dimen.ruler_start_padding);

//        float factor = DisplayDpiHelper.getInstance().getFake4kFactor();
//        DisplayMetrics metrics = getResources().getDisplayMetrics();
//        cmMeasurement = metrics.densityDpi / 2.54f / 10f / factor;
//        inchMeasurement = metrics.densityDpi / 10f / factor;
        cmMeasurement = getResources().getDimensionPixelOffset(R.dimen.ruler_cm_unit_distance);
        inchMeasurement = cmMeasurement * INCH_TO_CM;

        closeBtn = getResources().getDrawable(R.drawable.ic_ruler_close);
        extendShortExtendBtn = getResources().getDrawable(R.drawable.ic_ruler_extend);
        extendShortenBtn = getResources().getDrawable(R.drawable.ic_ruler_extend_extend_disabled);
        extendExtendBtn = getResources().getDrawable(R.drawable.ic_ruler_extend_shorten_disabled);
        extendBtn = extendShortExtendBtn;
        rotateBtn = getResources().getDrawable(R.drawable.ic_ruler_rotate);
        btnTopMargin = getResources().getDimensionPixelOffset(R.dimen.ruler_close_btn_vertical_padding);
        btnStartMargin = getResources().getDimensionPixelOffset(R.dimen.ruler_close_btn_horizontal_padding);
        btnMargin = getResources().getDimensionPixelOffset(R.dimen.ruler_btn_horizontal_padding);
        btnSize = getResources().getDimensionPixelOffset(R.dimen.ruler_btn_size);

        rulerLinePainter = new RulerLinePainter();
        bottomBarHeight = getResources().getDimensionPixelOffset(R.dimen.bottom_bar_height);
    }


    public Ruler(Context context, MeasurementToolListener listener) {
        super(context);
        name = RULER;
        this.rulerListener = listener;

        // init frame stroke
        @ColorInt int frameStrokeColor = context.getResources().getColor(R.color.ruler_frame_stroke_color);
        frameStrokeWidth = context.getResources().getDimensionPixelOffset(R.dimen.ruler_frame_stroke_width);
        rectPaint = initStrokePaint();
        rectPaint.setColor(frameStrokeColor);
        rectPaint.setStrokeWidth(frameStrokeWidth);

        rulerBackgroundPaint = initStrokePaint();
        rulerBackgroundPaint.setColor(getResources().getColor(R.color.ruler_background_color));
        rulerBackgroundPaint.setStyle(Paint.Style.FILL_AND_STROKE);

        measurePaint = initStrokePaint();
        measurePaint.setColor(frameStrokeColor);
        measurePaint.setStrokeWidth(frameStrokeWidth);

        measurementRectWidth = getResources().getDimensionPixelOffset(R.dimen.ruler_measurement_rect_width);
        measurementRectHeight = getResources().getDimensionPixelOffset(R.dimen.ruler_measurement_rect_height);
        measurementRectRadius = getResources().getDimensionPixelOffset(R.dimen.ruler_measurement_rect_radius);
        measurementRectPaint = initStrokePaint();
        measurementRectPaint.setStyle(Paint.Style.FILL);
        measurementRectPaint.setColor(getResources().getColor(R.color.ruler_measurement_background_color));

        measurementRectTextPaint = new TextPaint();
        measurementRectTextPaint.setAntiAlias(true);
        measurementRectTextPaint.setColor(getResources().getColor(R.color.hitevision_blue));
        measurementRectTextPaint.setTextSize(getResources().getDimensionPixelOffset(R.dimen.ruler_text_font_size));

        // init detect line
        @ColorInt int detectColor = context.getResources().getColor(R.color.ruler_detect_stroke_color);
        detectLineStrokeWidth = context.getResources().getDimensionPixelOffset(R.dimen.ruler_detect_range);
        detectLinePaint = initStrokePaint();
        detectLinePaint.setColor(detectColor);
        detectLinePaint.setStrokeWidth(detectLineStrokeWidth);

        textRectSize = context.getResources().getDimensionPixelSize(R.dimen.ruler_text_rect_size);
        textPaint = new TextPaint();
        textPaint.setColor(context.getResources().getColor(R.color.black));
        textPaint.setTextSize(context.getResources().getDimensionPixelSize(R.dimen.ruler_text_font_size));
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setAntiAlias(true);

        rectPoints = new PointF[4];
        for (int i = 0; i < rectPoints.length; i++) {
            rectPoints[i] = new PointF();
        }

        rulerDefaultWidth = context.getResources().getDimensionPixelOffset(R.dimen.ruler_default_width);
        rulerDefaultHeight = context.getResources().getDimensionPixelOffset(R.dimen.ruler_default_height);
        rulerMinLength = getResources().getDimensionPixelOffset(R.dimen.ruler_min_length);
        rulerMaxLength = getResources().getDimensionPixelOffset(R.dimen.ruler_max_length);

        cmLong = context.getResources().getDimensionPixelOffset(R.dimen.ruler_unit_long);
        cmMiddle = context.getResources().getDimensionPixelOffset(R.dimen.ruler_unit_middle);
        cmShort = context.getResources().getDimensionPixelOffset(R.dimen.ruler_unit_short);
        rulerStartPadding = context.getResources().getDimensionPixelOffset(R.dimen.ruler_start_padding);

//        float factor = DisplayDpiHelper.getInstance().getFake4kFactor();
//        DisplayMetrics metrics = getResources().getDisplayMetrics();
//        cmMeasurement = metrics.densityDpi / 2.54f / 10f / factor;
//        inchMeasurement = metrics.densityDpi / 10f / factor;
        cmMeasurement = getResources().getDimensionPixelOffset(R.dimen.ruler_cm_unit_distance);
        inchMeasurement = cmMeasurement * INCH_TO_CM;

        closeBtn = getResources().getDrawable(R.drawable.ic_ruler_close);
        extendShortExtendBtn = getResources().getDrawable(R.drawable.ic_ruler_extend);
        extendShortenBtn = getResources().getDrawable(R.drawable.ic_ruler_extend_extend_disabled);
        extendExtendBtn = getResources().getDrawable(R.drawable.ic_ruler_extend_shorten_disabled);
        extendBtn = extendShortExtendBtn;
        rotateBtn = getResources().getDrawable(R.drawable.ic_ruler_rotate);
        btnTopMargin = getResources().getDimensionPixelOffset(R.dimen.ruler_close_btn_vertical_padding);
        btnStartMargin = getResources().getDimensionPixelOffset(R.dimen.ruler_close_btn_horizontal_padding);
        btnMargin = getResources().getDimensionPixelOffset(R.dimen.ruler_btn_horizontal_padding);
        btnSize = getResources().getDimensionPixelOffset(R.dimen.ruler_btn_size);

        rulerLinePainter = new RulerLinePainter();
        bottomBarHeight = getResources().getDimensionPixelOffset(R.dimen.bottom_bar_height);
    }

    @Override
    public void loadMeasurementToolRecord(MeasurementToolRecord record) {
        if (record instanceof RulerRecord) {
            reset = false;
            initDefaultRuler();
            totalDegree = record.getRotateDegree();
            if (rectPoints != null && ((RulerRecord) record).getRectPoints() != null) {
                for (int i = 0; i < rectPoints.length; i++) {
                    rectPoints[i].set(((RulerRecord) record).getRectPoints()[i]);
                }
            }

            updateRectPath();
            updateRegion();
            update = true;
            invalidate();
        }
    }

    @Override
    public MeasurementToolRecord createMeasurementToolRecord() {
        return new RulerRecord(name, totalDegree, rectPoints);
    }

    public void setDrawingLineWidth(float width) {
        rulerLinePainter.paint.setStrokeWidth(width);
    }

    public boolean rulerOnTouch(View v, MotionEvent event, boolean canDraw) {
        int action = event.getActionMasked();
        float x = event.getX(0);
        float y = event.getY(0);
        boolean contain = false;

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                int intX = (int) x;
                int intY = (int) y;
                boolean hitTop = detectLineRegionTop.contains((int) x, (int) y) && canDraw;
                boolean hitBottom = detectLineRegionBottom.contains((int) x, (int) y) && canDraw;

                rulerAction = MeasurementAction.MOVE;

                if (rulerRegion.contains(intX, intY) || hitTop || hitBottom) {
                    contain = true;
                }
                if (contain) {
                    dragX = x;
                    dragY = y;

                    if (hitTop || hitBottom) {
                        rulerAction = MeasurementAction.DRAW;
                        rulerLinePainter.type = hitTop ? RulerDrawingType.TOP : RulerDrawingType.BOTTOM;
                        float[] xy = drawLine(x, y, true);
                        if (rulerListener != null) {
                            startP = new PointF(xy[0], xy[1]);
                            List<PointF> ls = new ArrayList<>();
                            ls.add(startP);
                            ls.add(new PointF(xy[0], xy[1]));
                            rulerListener.onDrawing(ls, false);
                        }
                    } else if (PointF.length(x - closeP.x, y - closeP.y) < btnSize / 2f) {
                        rulerAction = MeasurementAction.CLOSE;
                    } else if (PointF.length(x - extendP.x, y - extendP.y) < btnSize / 2f) {
                        rulerAction = MeasurementAction.EXTEND;
                    } else if (PointF.length(x - rotateP.x, y - rotateP.y) < btnSize / 2f) {
                        rulerAction = MeasurementAction.ROTATE;
                    }
                }
                return contain;
            }
            case MotionEvent.ACTION_POINTER_DOWN: {
                if (event.getPointerCount() <= 2 && rulerAction == MeasurementAction.MOVE) {
                    rulerAction = MeasurementAction.MULTI_FINGER;
                    dragX2 = event.getX(event.getActionIndex());
                    dragY2 = event.getY(event.getActionIndex());
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                switch (rulerAction) {
                    case DRAW: {
                        float[] xy = drawLine(x, y, false);
                        if (rulerListener != null) {
                            List<PointF> ls = new ArrayList<>();
                            ls.add(startP);
                            ls.add(new PointF(xy[0], xy[1]));
                            rulerListener.onDrawing(ls, false);
                        }
                        break;
                    }
                    case EXTEND: {
                        extend(x - dragX, y - dragY);
                        break;
                    }
                    case ROTATE: {
//                        rotate(x, dragX, y, dragY);
                        PointF center = new PointF((rectPoints[0].x + rectPoints[2].x) / 2f,
                                (rectPoints[0].y + rectPoints[2].y) / 2f);
                        motionCenterP.set(center.x, center.y);
                        rotate(x, dragX, y, dragY, center.x, center.x, center.y, center.y);
                        break;
                    }
                    case MOVE: {
                        move(x - dragX, y - dragY);
                        break;
                    }
                    case MULTI_FINGER: {
                        if (event.getPointerCount() >= 2) {
                            float x1 = event.getX(0);
                            float y1 = event.getY(0);
                            float x2 = event.getX(1);
                            float y2 = event.getY(1);
//                            scale(dragX, dragY, dragX2, dragY2, x1, y1, x2, y2);
                            multiFingerAction(dragX, dragY, dragX2, dragY2, x1, y1, x2, y2);
                            dragX2 = x2;
                            dragY2 = y2;
                        } else {
                            rulerAction = MeasurementAction.MOVE;
                        }
                        break;
                    }
                }
                measurementText = getMeasurementText(rulerAction);
                dragX = x;
                dragY = y;
                break;
            }
            case MotionEvent.ACTION_UP: {
                switch (rulerAction) {
                    case CLOSE: {
                        close();
                        break;
                    }
                    case DRAW: {
                        float[] xy = drawLine(x, y, false);
                        if (rulerListener != null) {
                            List<PointF> ls = new ArrayList<>();
                            ls.add(startP);
                            ls.add(new PointF(xy[0], xy[1]));
                            rulerListener.onDrawing(ls, true);
                        }
                        break;
                    }
                    case EXTEND: {
                        extend(x - dragX, y - dragY);
                        updateRegion();
                        break;
                    }
                    case ROTATE: {
//                        afterRotate(x, dragX, y, dragY);
                        PointF center = new PointF((rectPoints[0].x + rectPoints[2].x) / 2f,
                                (rectPoints[0].y + rectPoints[2].y) / 2f);
                        motionCenterP.set(center.x, center.y);
                        afterRotate(x, dragX, y, dragY, center.x, center.x, center.y, center.y);
                        updateRegion();
                        break;
                    }
                    case MOVE:
                    case MULTI_FINGER: {
                        afterMove(0, 0);
                        afterRotate(dragX, dragX, dragY, dragY, dragX2, dragX2, dragY2, dragY2);
                        updateRegion();
                        break;
                    }
                }
                if (rulerAction != MeasurementAction.DRAW) {
                    measurementText = "";
                }
                if (rulerListener != null) {
                    rulerListener.updateRecord(name);
                }
                break;
            }
        }

        return false;
    }

    private float[] drawLine(float x, float y, boolean init) {
        float dis = PointF.length(rectPoints[1].x - rectPoints[0].x, rectPoints[1].y - rectPoints[0].y);
        PointF v = new PointF((rectPoints[1].x - rectPoints[0].x) / dis, (rectPoints[1].y - rectPoints[0].y) / dis);
        PointF nv = new PointF(v.y, -v.x);
        float a = nv.x;
        float b = nv.y;
        float aabb = a * a + b * b;
        float offset = rulerLinePainter.paint.getStrokeWidth() / 2f + frameStrokeWidth;
        float c2 = b * x - a * y;
        float newX = x;
        float newY = y;

        if (rulerLinePainter.type == RulerDrawingType.TOP) {
            float c1 = a * (rectPoints[0].x + nv.x * offset) + b * (rectPoints[0].y + nv.y * offset);
            newX = (a * c1 + b * c2) / aabb;
            newY = (b * c1 - a * c2) / aabb;
            rulerLinePainter.end.set(newX, newY);
        } else {
            float c1 = a * (rectPoints[3].x - nv.x * offset) + b * (rectPoints[3].y - nv.y * offset);
            newX = (a * c1 + b * c2) / aabb;
            newY = (b * c1 - a * c2) / aabb;
            rulerLinePainter.end.set(newX, newY);
        }
        if (init) {
            rulerLinePainter.start.set(rulerLinePainter.end);
        }
        update = true;
        invalidate();

        return new float[]{newX, newY};
    }

    private void close() {
        OptionsRulerMenuAdapter.getLister(name).onClick(this);
        if (rulerListener != null) {
            rulerListener.onClose(name);
        }
    }

    private void extend(float dragX, float dragY) {
        PointF v = new PointF(rectPoints[1].x - rectPoints[0].x, rectPoints[1].y - rectPoints[0].y);
        float disV = PointF.length(rectPoints[1].x - rectPoints[0].x, rectPoints[1].y - rectPoints[0].y);
        v.x /= disV;
        v.y /= disV;
        float disDragOnV = dragX * v.x + dragY * v.y;
        float scaleX = v.x * disDragOnV;
        float scaleY = v.y * disDragOnV;

        float newDis = PointF.length(rectPoints[1].x + scaleX - rectPoints[0].x, rectPoints[1].y + scaleY - rectPoints[0].y);
        if (newDis <= rulerMinLength) {
            extendBtn = extendExtendBtn;
            rectPoints[1].x = rectPoints[0].x + v.x * rulerMinLength;
            rectPoints[1].y = rectPoints[0].y + v.y * rulerMinLength;
            rectPoints[2].x = rectPoints[3].x + v.x * rulerMinLength;
            rectPoints[2].y = rectPoints[3].y + v.y * rulerMinLength;
        } else if (newDis >= rulerMaxLength) {
            extendBtn = extendShortenBtn;
            rectPoints[1].x = rectPoints[0].x + v.x * rulerMaxLength;
            rectPoints[1].y = rectPoints[0].y + v.y * rulerMaxLength;
            rectPoints[2].x = rectPoints[3].x + v.x * rulerMaxLength;
            rectPoints[2].y = rectPoints[3].y + v.y * rulerMaxLength;
        } else {
            extendBtn = extendShortExtendBtn;
            rectPoints[1].x += scaleX;
            rectPoints[1].y += scaleY;
            rectPoints[2].x += scaleX;
            rectPoints[2].y += scaleY;
        }

        updateRectPath();
        update = true;
        invalidate();
    }

    private void move(float x, float y) {
        float centerY = (rectPoints[0].y + rectPoints[2].y) / 2f;

        if (centerY + translateY + y > this.getHeight() - bottomBarHeight) {
            y = this.getHeight() - bottomBarHeight - centerY - translateY;
        }

        translateX += x;
        translateY += y;

        update = true;
        invalidate();
    }

    private void afterMove(float x, float y) {
        move(x, y);
        for (PointF rectPoint : rectPoints) {
            rectPoint.x += translateX;
            rectPoint.y += translateY;
        }
        translateX = 0;
        translateY = 0;
        updateRectPath();
        update = true;
        invalidate();
    }

    private void rotate(float x, float dragX, float y, float dragY, float x2, float dragX2, float y2, float dragY2) {
        PointF v1 = new PointF(x - x2, y - y2);
        PointF v2 = new PointF(dragX - dragX2, dragY - dragY2);
        float arc1 = (float) Math.atan2(v1.x, v1.y);
        float arc2 = (float) Math.atan2(v2.x, v2.y);
        float d = (float) Math.toDegrees(arc2 - arc1);
        canvasDegree += d;

        update = true;
        invalidate();
    }

    private void afterRotate(float x, float dragX, float y, float dragY, float x2, float dragX2, float y2, float dragY2) {
        PointF v1 = new PointF(x - x2, y - y2);
        PointF v2 = new PointF(dragX - dragX2, dragY - dragY2);
        float arc1 = (float) Math.atan2(v1.x, v1.y);
        float arc2 = (float) Math.atan2(v2.x, v2.y);
        float d = (float) Math.toDegrees(arc2 - arc1);
        canvasDegree += d;

        canvasDegree += getDegreeStopOffset(totalDegree + canvasDegree, ROTATE_SKIP, ROTATE_UNIT);

        Matrix matrix = new Matrix();
        float[] p = new float[]{rectPoints[0].x, rectPoints[0].y, rectPoints[1].x, rectPoints[1].y,
                rectPoints[2].x, rectPoints[2].y, rectPoints[3].x, rectPoints[3].y};
        matrix.setRotate(canvasDegree, motionCenterP.x, motionCenterP.y);
        matrix.mapPoints(p);

        totalDegree += canvasDegree;
        canvasDegree = 0;

        rectPoints[0].set(p[0], p[1]);
        rectPoints[1].set(p[2], p[3]);
        rectPoints[2].set(p[4], p[5]);
        rectPoints[3].set(p[6], p[7]);

        updateRectPath();
        update = true;
        invalidate();
    }

    private void multiFingerAction(float dragX, float dragY, float dragX2, float dragY2, float x1, float y1, float x2, float y2) {
        float offsetX = (((x1 + x2) / 2f) - ((dragX + dragX2) / 2f));
        float offsetY = (((y1 + y2) / 2f) - ((dragY + dragY2) / 2f));
        afterMove(offsetX, offsetY);
        motionCenterP.set((x1 + x2) / 2f, (y1 + y2) / 2f);

        rotate(x1, dragX, y1, dragY, x2, dragX2, y2, dragY2);
        updateRectPath();
        update = true;
        invalidate();
    }

    private void updateRectPath() {
        rulerRectPath.rewind();
        rulerRectPath.moveTo(rectPoints[0].x, rectPoints[0].y);
        rulerRectPath.lineTo(rectPoints[1].x, rectPoints[1].y);
        rulerRectPath.lineTo(rectPoints[2].x, rectPoints[2].y);
        rulerRectPath.lineTo(rectPoints[3].x, rectPoints[3].y);
        rulerRectPath.lineTo(rectPoints[0].x, rectPoints[0].y);
    }

    @Override
    protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
        super.onVisibilityChanged(changedView, visibility);
        if (visibility == VISIBLE) {
            update = true;
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (this.getVisibility() != VISIBLE) {
            return;
        }

        if (reset) {
            reset = false;
            initDefaultRuler();
            update = true;
            if (rulerListener != null) {
                rulerListener.updateRecord(name);
            }
        }

        // update ruler appearance
        if (update) {
            update = false;

            PointF center = new PointF((rectPoints[0].x + rectPoints[2].x) / 2f, (rectPoints[0].y + rectPoints[2].y) / 2f);
            canvas.save();
            if (canvasDegree != 0) {
                canvas.rotate(canvasDegree + getDegreeStopOffset(canvasDegree + totalDegree, ROTATE_SKIP, ROTATE_UNIT),
                        motionCenterP.x, motionCenterP.y);
            }

            if (translateX != 0 || translateY != 0) {
                canvas.translate(translateX, translateY);
            }

            // draw ruler frame and detection path

            canvas.drawPath(rulerRectPath, rulerBackgroundPaint);
            canvas.drawPath(rulerRectPath, rectPaint);
            canvas.drawLine(rectPoints[0].x, rectPoints[0].y, rectPoints[1].x, rectPoints[1].y, detectLinePaint);
            canvas.drawLine(rectPoints[2].x, rectPoints[2].y, rectPoints[3].x, rectPoints[3].y, detectLinePaint);

            // draw measurement helper

            canvas.save();
            canvas.rotate(totalDegree, center.x, center.y);
            RectF rectF = new RectF(center.x - measurementRectWidth / 2f, center.y - measurementRectHeight / 2f,
                    center.x + measurementRectWidth / 2f, center.y + measurementRectHeight / 2f);
            canvas.drawRoundRect(rectF, measurementRectRadius, measurementRectRadius, measurementRectPaint);
            float tw = measurementRectTextPaint.measureText(measurementText);
            float th = TextLayoutUtil.getHeightOfString(measurementRectTextPaint, measurementText);
            canvas.drawText(measurementText, center.x - tw / 2f, center.y + th / 2f, measurementRectTextPaint);
            canvas.restore();

            // draw cm measurement

            PointF nVector = new PointF();
            PointF vector = new PointF();

            float distance = PointF.length(rectPoints[1].x - rectPoints[0].x, rectPoints[1].y - rectPoints[0].y);
            vector.x = (rectPoints[1].x - rectPoints[0].x) / distance;
            vector.y = (rectPoints[1].y - rectPoints[0].y) / distance;
            nVector.x = -vector.y;
            nVector.y = vector.x;

            float xPadding = vector.x * cmMeasurement;
            float yPadding = vector.y * cmMeasurement;
            float nxPaddingLong = nVector.x * cmLong;
            float nyPaddingLong = nVector.y * cmLong;
            float nxPaddingMiddle = nVector.x * cmMiddle;
            float nyPaddingMiddle = nVector.y * cmMiddle;
            float nxPaddingShort = nVector.x * cmShort;
            float nyPaddingShort = nVector.y * cmShort;
            float sx = rectPoints[0].x + vector.x * rulerStartPadding;
            float sy = rectPoints[0].y + vector.y * rulerStartPadding;
            float dx;
            float dy;
            float endDistance = PointF.length(rectPoints[1].x - sx, rectPoints[1].y - sy);
            int count = 0;
            int cm = 0;

            while (endDistance >= rulerStartPadding && count <= 400) {
                if (count % 10 == 0) {
                    dx = sx + nxPaddingLong;
                    dy = sy + nyPaddingLong;
                    String cmStr = String.format(Locale.ENGLISH, "%d", cm);
                    float cmStrW = textPaint.measureText(cmStr);
                    float tx = dx + (nVector.x * textRectSize) - (vector.x * cmStrW / 2f);
                    float ty = dy + (nVector.y * textRectSize) - (vector.y * cmStrW / 2f);
                    canvas.save();
                    canvas.rotate(totalDegree, tx, ty);
                    canvas.drawText(cmStr, tx, ty, textPaint);
                    canvas.restore();
                    cm++;
                } else if (count % 5 == 0) {
                    dx = sx + nxPaddingMiddle;
                    dy = sy + nyPaddingMiddle;
                } else {
                    dx = sx + nxPaddingShort;
                    dy = sy + nyPaddingShort;
                }
                canvas.drawLine(sx, sy, dx, dy, measurePaint);

                count++;
                sx += xPadding;
                sy += yPadding;
                endDistance = PointF.length(rectPoints[1].x - sx, rectPoints[1].y - sy);
            }

            // draw buttons

            float bx = rectPoints[3].x + vector.x * btnStartMargin - nVector.x * btnTopMargin;
            float by = rectPoints[3].y + vector.y * btnStartMargin - nVector.y * btnTopMargin;
            float btnHalfSize = btnSize / 2f;
            canvas.save();
            canvas.rotate(totalDegree, bx, by);
            float offsetX = (bx - btnHalfSize) - (int) (bx - btnHalfSize);
            float offsetY = (by - btnHalfSize) - (int) (by - btnHalfSize);
            closeP.set(bx, by);
            closeBtn.setBounds((int) (bx - btnHalfSize), (int) (by - btnHalfSize),
                    (int) (bx + btnHalfSize), (int) (by + btnHalfSize));
            canvas.translate(offsetX, offsetY);
            closeBtn.draw(canvas);
            canvas.restore();

            bx = rectPoints[2].x - vector.x * btnStartMargin - nVector.x * btnTopMargin;
            by = rectPoints[2].y - vector.y * btnStartMargin - nVector.y * btnTopMargin;
            canvas.save();
            canvas.rotate(totalDegree, bx, by);
            offsetX = (bx - btnHalfSize) - (int) (bx - btnHalfSize);
            offsetY = (by - btnHalfSize) - (int) (by - btnHalfSize);
            extendP.set(bx, by);
            extendBtn.setBounds((int) (bx - btnHalfSize), (int) (by - btnHalfSize),
                    (int) (bx + btnHalfSize), (int) (by + btnHalfSize));
            canvas.translate(offsetX, offsetY);
            extendBtn.draw(canvas);
            canvas.restore();

            bx -= vector.x * btnMargin;
            by -= vector.y * btnMargin;
            canvas.save();
            canvas.rotate(totalDegree, bx, by);
            offsetX = (bx - btnHalfSize) - (int) (bx - btnHalfSize);
            offsetY = (by - btnHalfSize) - (int) (by - btnHalfSize);
            rotateP.set(bx, by);
            rotateBtn.setBounds((int) (bx - btnHalfSize), (int) (by - btnHalfSize),
                    (int) (bx + btnHalfSize), (int) (by + btnHalfSize));
            canvas.translate(offsetX, offsetY);
            rotateBtn.draw(canvas);
            canvas.restore();

            // inch measurement

            vector.x *= -1;
            vector.y *= -1;
            nVector.x *= -1;
            nVector.y *= -1;
            xPadding = vector.x * inchMeasurement;
            yPadding = vector.y * inchMeasurement;
            nxPaddingLong *= -1;
            nyPaddingLong *= -1;
            nxPaddingMiddle *= -1;
            nyPaddingMiddle *= -1;
            nxPaddingShort *= -1;
            nyPaddingShort *= -1;
            sx = rectPoints[2].x + vector.x * rulerStartPadding;
            sy = rectPoints[2].y + vector.y * rulerStartPadding;
            endDistance = PointF.length(rectPoints[3].x - sx, rectPoints[3].y - sy);

            count = 0;
            int inch = 0;
            while (endDistance >= rulerStartPadding) {
                if (count % 10 == 0) {
                    dx = sx + nxPaddingLong;
                    dy = sy + nyPaddingLong;
                    String inchStr = String.format(Locale.ENGLISH, "%d", inch);
                    float inchStrW = textPaint.measureText(inchStr);
                    float tx = dx + (nVector.x * textRectSize) - (vector.x * inchStrW / 2f);
                    float ty = dy + (nVector.y * textRectSize) - (vector.y * inchStrW / 2f);
                    canvas.save();
                    canvas.rotate(totalDegree + 180, tx, ty);
                    canvas.drawText(inchStr, tx, ty, textPaint);
                    canvas.restore();
                    inch++;
                } else if (count % 5 == 0) {
                    dx = sx + nxPaddingMiddle;
                    dy = sy + nyPaddingMiddle;
                } else {
                    dx = sx + nxPaddingShort;
                    dy = sy + nyPaddingShort;
                }
                canvas.drawLine(sx, sy, dx, dy, measurePaint);

                count++;
                sx += xPadding;
                sy += yPadding;
                endDistance = PointF.length(rectPoints[3].x - sx, rectPoints[3].y - sy);
            }

            canvas.restore();
        }
    }

    private void initDefaultRuler() {
        if (startPointF == null) {
            startPointF = new PointF();
        }
        this.measure(0, 0);
        float viewWidth = this.getWidth();
        float viewHeight = this.getHeight();
        startPointF.x = viewWidth / 2f;
        startPointF.y = viewHeight / 2f;
        canvasDegree = 0f;
        totalDegree = 0;

        float rulerWidth = rulerDefaultWidth;
        float rulerHeight = rulerDefaultHeight;

        float left = startPointF.x - rulerWidth / 2f;
        float top = startPointF.y - rulerHeight / 2f;
        float right = startPointF.x + rulerWidth / 2f;
        float bottom = startPointF.y + rulerHeight / 2f;
        rectPoints[0].x = left;
        rectPoints[0].y = top;
        rectPoints[1].x = right;
        rectPoints[1].y = top;
        rectPoints[2].x = right;
        rectPoints[2].y = bottom;
        rectPoints[3].x = left;
        rectPoints[3].y = bottom;

        rulerRectPath = new Path();
        updateRectPath();

        rulerRegion = new Region();
        detectLineRegionTop = new Region();
        detectLineRegionBottom = new Region();
        updateRegion();
    }

    private void updateRegion() {
        // ruler main rect
        Rect rect = new Rect();
        RectF rectF = new RectF();
        Region clipRegion = new Region();
        rulerRectPath.computeBounds(rectF, true);
        rectF.roundOut(rect);
        clipRegion.set(rect);
        rulerRegion.setPath(rulerRectPath, clipRegion);

        // ruler top detection region
        float distance = PointF.length(rectPoints[1].x - rectPoints[0].x, rectPoints[1].y - rectPoints[0].y);
        PointF nVector = new PointF();
        PointF vector = new PointF();
        vector.x = (rectPoints[1].x - rectPoints[0].x) / distance;
        vector.y = (rectPoints[1].y - rectPoints[0].y) / distance;
        nVector.x = -vector.y;
        nVector.y = vector.x;
        float width = detectLineStrokeWidth / 2f;
        float p0x = rectPoints[0].x + nVector.x * width;
        float p0y = rectPoints[0].y + nVector.y * width;
        float p1x = rectPoints[1].x + nVector.x * width;
        float p1y = rectPoints[1].y + nVector.y * width;
        float p2x = rectPoints[1].x - nVector.x * width;
        float p2y = rectPoints[1].y - nVector.y * width;
        float p3x = rectPoints[0].x - nVector.x * width;
        float p3y = rectPoints[0].y - nVector.y * width;
        Path path = new Path();
        path.moveTo(p0x, p0y);
        path.lineTo(p1x, p1y);
        path.lineTo(p2x, p2y);
        path.lineTo(p3x, p3y);
        path.lineTo(p0x, p0y);
        path.addCircle(rectPoints[0].x, rectPoints[0].y, width, Path.Direction.CCW);
        path.addCircle(rectPoints[1].x, rectPoints[1].y, width, Path.Direction.CCW);
        path.computeBounds(rectF, true);
        rectF.roundOut(rect);
        clipRegion.set(rect);
        detectLineRegionTop.setPath(path, clipRegion);

        // ruler bottom detection region
        p0x = rectPoints[3].x + nVector.x * width;
        p0y = rectPoints[3].y + nVector.y * width;
        p1x = rectPoints[2].x + nVector.x * width;
        p1y = rectPoints[2].y + nVector.y * width;
        p2x = rectPoints[2].x - nVector.x * width;
        p2y = rectPoints[2].y - nVector.y * width;
        p3x = rectPoints[3].x - nVector.x * width;
        p3y = rectPoints[3].y - nVector.y * width;
        path.rewind();
        path.moveTo(p0x, p0y);
        path.moveTo(p0x, p0y);
        path.lineTo(p1x, p1y);
        path.lineTo(p2x, p2y);
        path.lineTo(p3x, p3y);
        path.lineTo(p0x, p0y);
        path.addCircle(rectPoints[2].x, rectPoints[2].y, width, Path.Direction.CCW);
        path.addCircle(rectPoints[3].x, rectPoints[3].y, width, Path.Direction.CCW);
        path.computeBounds(rectF, true);
        rectF.roundOut(rect);
        clipRegion.set(rect);
        detectLineRegionBottom.setPath(path, clipRegion);
    }

    private String getMeasurementText(MeasurementAction action) {
        if (action == MeasurementAction.DRAW) {
            return String.format(Locale.ENGLISH, "%.1f", rulerLinePainter.getDistance());
        }
        if (action == MeasurementAction.ROTATE || action == MeasurementAction.MULTI_FINGER) {
            float degree = -(totalDegree + canvasDegree + getDegreeStopOffset(totalDegree + canvasDegree, ROTATE_SKIP, ROTATE_UNIT));
            float angle = (degree + 360) % 360;
            return String.format(Locale.ENGLISH, "%.1f°", angle);
        }
        return "";
    }

    private class RulerLinePainter {
        PointF start = new PointF();
        PointF end = new PointF();
        Paint paint;
        RulerDrawingType type;

        RulerLinePainter() {
            paint = initStrokePaint();
        }

        float getDistance() {
            float raw = PointF.length(start.x - end.x, start.y - end.y);
            return type == RulerDrawingType.TOP ? raw / cmMeasurement / 10f : raw / inchMeasurement / 10f;
        }
    }

    enum RulerDrawingType {
        NONE, TOP, BOTTOM
    }
}
